/**
 *    Copyright 2009-2018 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.yinsin.jpabatis.executor.resultset;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import javax.persistence.Query;

import org.hibernate.SQLQuery;
import org.hibernate.transform.Transformers;

import com.yinsin.jpabatis.annotations.AutomapConstructor;
import com.yinsin.jpabatis.binging.MapperMethod.ParamMap;
import com.yinsin.jpabatis.cache.CacheKey;
import com.yinsin.jpabatis.config.Configuration;
import com.yinsin.jpabatis.exceptions.JpaBatisException;
import com.yinsin.jpabatis.executor.ErrorContext;
import com.yinsin.jpabatis.executor.Executor;
import com.yinsin.jpabatis.executor.ExecutorException;
import com.yinsin.jpabatis.executor.loader.ResultLoader;
import com.yinsin.jpabatis.executor.loader.ResultLoaderMap;
import com.yinsin.jpabatis.executor.parameter.ParameterHandler;
import com.yinsin.jpabatis.executor.result.DefaultResultContext;
import com.yinsin.jpabatis.mapper.BoundSql;
import com.yinsin.jpabatis.mapper.Discriminator;
import com.yinsin.jpabatis.mapper.MappedStatement;
import com.yinsin.jpabatis.mapper.ResultMap;
import com.yinsin.jpabatis.mapper.ResultMapping;
import com.yinsin.jpabatis.reflection.MetaObject;
import com.yinsin.jpabatis.reflection.ReflectorFactory;
import com.yinsin.jpabatis.reflection.factory.ObjectFactory;
import com.yinsin.jpabatis.session.AutoMappingBehavior;
import com.yinsin.jpabatis.session.ResultContext;
import com.yinsin.jpabatis.session.ResultHandler;
import com.yinsin.jpabatis.session.RowBounds;
import com.yinsin.jpabatis.type.JdbcType;
import com.yinsin.jpabatis.type.TypeHandler;
import com.yinsin.jpabatis.type.TypeHandlerRegistry;
import com.yinsin.jpabatis.util.BeanUtils;

/**
 * @author Clinton Begin
 * @author Eduardo Macarron
 * @author Iwao AVE!
 * @author Kazuki Shimizu
 */
public class DefaultResultSetHandler implements ResultSetHandler {

	private static final Object DEFERRED = new Object();

	private final Executor executor;
	private final Configuration configuration;
	private final MappedStatement mappedStatement;
	private final RowBounds rowBounds;
	private final ParameterHandler parameterHandler;
	private final ResultHandler<?> resultHandler;
	private final BoundSql boundSql;
	private final TypeHandlerRegistry typeHandlerRegistry;
	private final ObjectFactory objectFactory;
	private final ReflectorFactory reflectorFactory;

	// nested resultmaps
	private final Map<CacheKey, Object> nestedResultObjects = new HashMap<>();
	private final Map<String, Object> ancestorObjects = new HashMap<>();
	private Object previousRowValue;

	// multiple resultsets
	private final ResultMapping resultMapping = null;
	private final Map<CacheKey, List<PendingRelation>> pendingRelations = new HashMap<>();

	private static class PendingRelation {
		public MetaObject metaObject;
		public ResultMapping propertyMapping;
	}

	public DefaultResultSetHandler(Executor executor, MappedStatement mappedStatement, ParameterHandler parameterHandler, ResultHandler<?> resultHandler, BoundSql boundSql,
			RowBounds rowBounds) {
		this.executor = executor;
		this.configuration = mappedStatement.getConfiguration();
		this.mappedStatement = mappedStatement;
		this.rowBounds = rowBounds;
		this.parameterHandler = parameterHandler;
		this.boundSql = boundSql;
		this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
		this.objectFactory = configuration.getObjectFactory();
		this.reflectorFactory = configuration.getReflectorFactory();
		this.resultHandler = resultHandler;
	}

	//
	// HANDLE RESULT SETS
	//
	@SuppressWarnings("unchecked")
	@Override
	public List<Object> handleResultSets(Query query) throws JpaBatisException {
		ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
		query.unwrap(SQLQuery.class).setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP);
		List<Map<String, Object>> listT = query.getResultList();
		int resultMapCount = 0;
		if (null != listT) {
			resultMapCount = listT.size();
		}

		final List<Object> multipleResults = new ArrayList<Object>();
		int resultSetCount = 0;
		ResultMap resultMap = mappedStatement.getResultMap();
		ResultSetWrapper rsw = null;
		if (resultMapCount > 0) {
			rsw = getNextResultSet(listT.get(resultSetCount), resultMap);
			validateResultMapsCount(rsw, null == mappedStatement ? 0 : 1);
			parseResultObject(rsw, resultMap, multipleResults);
			resultSetCount++;
		}
		
		while (rsw != null && resultMapCount > resultSetCount) {
			rsw = getNextResultSet(listT.get(resultSetCount), resultMap);
			parseResultObject(rsw, resultMap, multipleResults);
			cleanUpAfterHandlingResultSet();
			resultSetCount++;
		}
		return multipleResults;
	}

	@SuppressWarnings("unchecked")
	@Override
	public Object handleResultSet(Query query) throws JpaBatisException {
		List<Object> multipleResults = handleResultSets(query);
		return collapseSingleResultList(multipleResults);
	}

	private ResultSetWrapper getNextResultSet(Map<String, Object> result, ResultMap resultMap) throws JpaBatisException {
		return result != null ? new ResultSetWrapper(result, configuration, resultMap) : null;
	}

	@SuppressWarnings("unchecked")
	private void parseResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults) throws JpaBatisException {
		try {
			Class<?> typeClass = resultMap.getType();
			if (!typeClass.getSimpleName().equals("Map") && !typeClass.getSimpleName().equals("HashMap")) {
				Object object = typeClass.newInstance();
				List<String> columnList = rsw.getColumnNames();
				Class<?> classz = null;
				String methodName = null;
				Method method = null;
				for (String column : columnList) {
					classz = rsw.getJavaType(column);
					methodName = rsw.getMethodName(column);
					if(null != classz){
						try {
							method = typeClass.getMethod(methodName, new Class[] { classz });
							method.invoke(object, new Object[] { rsw.getValue(column, classz) });
						} catch (Exception e) {
						}
					}
				}
				multipleResults.add(object);
			} else {
				Map<String, Object> resMap = (Map<String, Object>) rsw.getResultObject();
				Map<String, Object> value = new HashMap<String, Object>();
				List<String> columnList = rsw.getColumnNames();
				Class<?> classz = null;
				for (String column : columnList) {
					classz = rsw.getJavaType(column);
					value.put(rsw.getProperty(column), BeanUtils.coventValue(resMap.get(column), classz));
				}
				multipleResults.add(value);
			}
		} catch (Exception e) {
			throw new JpaBatisException(e);
		}
	}
	
	private void cleanUpAfterHandlingResultSet() {
		nestedResultObjects.clear();
	}

	private void validateResultMapsCount(ResultSetWrapper rsw, int resultMapCount) {
		if (rsw != null && resultMapCount < 1) {
			throw new ExecutorException("A query was run and no Result Maps were found for the Mapped Statement '" + mappedStatement.getId()
					+ "'.  It's likely that neither a Result Type nor a Result Map was specified.");
		}
	}
	
	private Object collapseSingleResultList(List<Object> multipleResults) {
		return multipleResults.size() >= 1 ? multipleResults.get(0) : null;
	}

	protected void checkResultHandler() {
		if (resultHandler != null && configuration.isSafeResultHandlerEnabled() && !mappedStatement.isResultOrdered()) {
			throw new ExecutorException("Mapped Statements with nested result mappings cannot be safely used with a custom ResultHandler. "
					+ "Use safeResultHandlerEnabled=false setting to bypass this check " + "or ensure your statement returns ordered data and set resultOrdered=true on it.");
		}
	}

	@SuppressWarnings("unchecked" /*
								 * because ResultHandler<?> is always
								 * ResultHandler<Object>
								 */)
	private void callResultHandler(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue) {
		resultContext.nextResultObject(rowValue);
		((ResultHandler<Object>) resultHandler).handleResult(resultContext);
	}

	private boolean shouldProcessMoreRows(ResultContext<?> context, RowBounds rowBounds) {
		return !context.isStopped() && context.getResultCount() < rowBounds.getLimit();
	}

	private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {
		if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {
			if (rowBounds.getOffset() != 0) {
				rs.absolute(rowBounds.getOffset());
			}
		} else {
			for (int i = 0; i < rowBounds.getOffset(); i++) {
				if (!rs.next()) {
					break;
				}
			}
		}
	}

	private boolean shouldApplyAutomaticMappings(ResultMap resultMap, boolean isNested) {
		if (resultMap.getAutoMapping() != null) {
			return resultMap.getAutoMapping();
		} else {
			if (isNested) {
				return AutoMappingBehavior.FULL == configuration.getAutoMappingBehavior();
			} else {
				return AutoMappingBehavior.NONE != configuration.getAutoMappingBehavior();
			}
		}
	}

	private CacheKey createKeyForMultipleResults(ResultSet rs, ResultMapping resultMapping, String names, String columns) throws SQLException {
		CacheKey cacheKey = new CacheKey();
		cacheKey.update(resultMapping);
		if (columns != null && names != null) {
			String[] columnsArray = columns.split(",");
			String[] namesArray = names.split(",");
			for (int i = 0; i < columnsArray.length; i++) {
				Object value = rs.getString(columnsArray[i]);
				if (value != null) {
					cacheKey.update(namesArray[i]);
					cacheKey.update(value);
				}
			}
		}
		return cacheKey;
	}

	private Constructor<?> findDefaultConstructor(final Constructor<?>[] constructors) {
		if (constructors.length == 1)
			return constructors[0];

		for (final Constructor<?> constructor : constructors) {
			if (constructor.isAnnotationPresent(AutomapConstructor.class)) {
				return constructor;
			}
		}
		return null;
	}

	private boolean allowedConstructorUsingTypeHandlers(final Constructor<?> constructor, final List<JdbcType> jdbcTypes) {
		final Class<?>[] parameterTypes = constructor.getParameterTypes();
		if (parameterTypes.length != jdbcTypes.size())
			return false;
		for (int i = 0; i < parameterTypes.length; i++) {
			if (!typeHandlerRegistry.hasTypeHandler(parameterTypes[i], jdbcTypes.get(i))) {
				return false;
			}
		}
		return true;
	}

	//
	// NESTED QUERY
	//

	private Object getNestedQueryConstructorValue(ResultSet rs, ResultMapping constructorMapping, String columnPrefix) throws SQLException {
		final String nestedQueryId = constructorMapping.getNestedQueryId();
		final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
		final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
		final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, constructorMapping, nestedQueryParameterType, columnPrefix);
		Object value = null;
		if (nestedQueryParameterObject != null) {
			final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
			final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);
			final Class<?> targetType = constructorMapping.getJavaType();
			final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
			value = resultLoader.loadResult();
		}
		return value;
	}

	private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
			throws SQLException {
		final String nestedQueryId = propertyMapping.getNestedQueryId();
		final String property = propertyMapping.getProperty();
		final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
		final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
		final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping, nestedQueryParameterType, columnPrefix);
		Object value = null;
		if (nestedQueryParameterObject != null) {
			final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
			final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);
			final Class<?> targetType = propertyMapping.getJavaType();
			if (executor.isCached(nestedQuery, key)) {
				value = DEFERRED;
			} else {
				final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
				if (propertyMapping.isLazy()) {
					lazyLoader.addLoader(property, metaResultObject, resultLoader);
					value = DEFERRED;
				} else {
					value = resultLoader.loadResult();
				}
			}
		}
		return value;
	}

	private Object prepareParameterForNestedQuery(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType, String columnPrefix) throws SQLException {
		if (resultMapping.isCompositeResult()) {
			return prepareCompositeKeyParameter(rs, resultMapping, parameterType, columnPrefix);
		} else {
			return prepareSimpleKeyParameter(rs, resultMapping, parameterType, columnPrefix);
		}
	}

	private Object prepareSimpleKeyParameter(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType, String columnPrefix) throws SQLException {
		final TypeHandler<?> typeHandler;
		if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
			typeHandler = typeHandlerRegistry.getTypeHandler(parameterType);
		} else {
			typeHandler = typeHandlerRegistry.getUnknownTypeHandler();
		}
		return typeHandler.getResult(rs, prependPrefix(resultMapping.getColumn(), columnPrefix));
	}

	private Object prepareCompositeKeyParameter(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType, String columnPrefix) throws SQLException {
		final Object parameterObject = instantiateParameterObject(parameterType);
		final MetaObject metaObject = configuration.newMetaObject(parameterObject);
		boolean foundValues = false;
		for (ResultMapping innerResultMapping : resultMapping.getComposites()) {
			final Class<?> propType = metaObject.getSetterType(innerResultMapping.getProperty());
			final TypeHandler<?> typeHandler = typeHandlerRegistry.getTypeHandler(propType);
			final Object propValue = typeHandler.getResult(rs, prependPrefix(innerResultMapping.getColumn(), columnPrefix));
			// issue #353 & #560 do not execute nested query if key is null
			if (propValue != null) {
				metaObject.setValue(innerResultMapping.getProperty(), propValue);
				foundValues = true;
			}
		}
		return foundValues ? parameterObject : null;
	}

	private Object instantiateParameterObject(Class<?> parameterType) {
		if (parameterType == null) {
			return new HashMap<>();
		} else if (ParamMap.class.equals(parameterType)) {
			return new HashMap<>(); // issue #649
		} else {
			return objectFactory.create(parameterType);
		}
	}

	//
	// DISCRIMINATOR
	//

	public ResultMap resolveDiscriminatedResultMap(ResultSet rs, ResultMap resultMap, String columnPrefix) throws SQLException {
		Set<String> pastDiscriminators = new HashSet<>();
		Discriminator discriminator = resultMap.getDiscriminator();
		while (discriminator != null) {
			final Object value = getDiscriminatorValue(rs, discriminator, columnPrefix);
			final String discriminatedMapId = discriminator.getMapIdFor(String.valueOf(value));
			if (configuration.hasResultMap(discriminatedMapId)) {
				resultMap = configuration.getResultMap(discriminatedMapId);
				Discriminator lastDiscriminator = discriminator;
				discriminator = resultMap.getDiscriminator();
				if (discriminator == lastDiscriminator || !pastDiscriminators.add(discriminatedMapId)) {
					break;
				}
			} else {
				break;
			}
		}
		return resultMap;
	}

	private Object getDiscriminatorValue(ResultSet rs, Discriminator discriminator, String columnPrefix) throws SQLException {
		final ResultMapping resultMapping = discriminator.getResultMapping();
		final TypeHandler<?> typeHandler = resultMapping.getTypeHandler();
		return typeHandler.getResult(rs, prependPrefix(resultMapping.getColumn(), columnPrefix));
	}

	private String prependPrefix(String columnName, String prefix) {
		if (columnName == null || columnName.length() == 0 || prefix == null || prefix.length() == 0) {
			return columnName;
		}
		return prefix + columnName;
	}

	private void putAncestor(Object resultObject, String resultMapId) {
		ancestorObjects.put(resultMapId, resultObject);
	}

	private String getColumnPrefix(String parentPrefix, ResultMapping resultMapping) {
		final StringBuilder columnPrefixBuilder = new StringBuilder();
		if (parentPrefix != null) {
			columnPrefixBuilder.append(parentPrefix);
		}
		if (resultMapping.getColumnPrefix() != null) {
			columnPrefixBuilder.append(resultMapping.getColumnPrefix());
		}
		return columnPrefixBuilder.length() == 0 ? null : columnPrefixBuilder.toString().toUpperCase(Locale.ENGLISH);
	}

	private ResultMap getNestedResultMap(ResultSet rs, String nestedResultMapId, String columnPrefix) throws SQLException {
		ResultMap nestedResultMap = configuration.getResultMap(nestedResultMapId);
		return resolveDiscriminatedResultMap(rs, nestedResultMap, columnPrefix);
	}

}
